﻿using System;
using System.ServiceModel;
using SilverCrmSoap.CrmSdk;
using SilverCrmSoap.FetchXml;
using Entity = SilverCrmSoap.CrmSdk.Entity;

namespace SilverCrmSoap.Helpers
{
    /// <summary>
    /// SoapHelper: The class to help build Silverlight application for CRM using the Soap WebService.
    /// </summary>
    public static class SoapHelper
    {
        private static Uri _soapServer;
        private static OrganizationServiceClient _soapClient;

        /// <summary>
        /// ServerUri: CRM Server URI - If none is set, it will attempt to load it using the global contect.
        /// </summary>
        public static Uri ServerUri
        {
            get { return _soapServer ?? (_soapServer = XrmHelper.ServerUri); }
            set { _soapServer = value; }
        }

        /// <summary>
        /// SoapUri: CRM Soap URI - The Soap WebService.
        /// </summary>
        public static Uri SoapUri
        {
            get { return CombineUrl(ServerUri, "/XRMServices/2011/Organization.svc/web"); }
        }

        /// <summary>
        /// SoapClient: CRM Soap Client - If there is not a connection bound, then it will bind a new connection.
        /// </summary>
        public static OrganizationServiceClient SoapClient
        {
            get
            {
                if (_soapClient != null) return _soapClient;

                _soapClient = Bind();
                return _soapClient;
            }
        }

        /// <summary>
        /// Bind: Bind a connection to the Soap WebService.
        /// </summary>
        public static OrganizationServiceClient Bind()
        {
            // Setup binding
            var binding = new BasicHttpBinding(Uri.UriSchemeHttps == SoapUri.Scheme
                                                   ? BasicHttpSecurityMode.Transport
                                                   : BasicHttpSecurityMode.TransportCredentialOnly)
                              {
                                  MaxReceivedMessageSize = int.MaxValue,
                                  MaxBufferSize = int.MaxValue,
                                  SendTimeout = TimeSpan.FromMinutes(2)
                              };

            // Abstantiate Client
            return new OrganizationServiceClient(binding, new EndpointAddress(SoapUri));

        }
        
        /// <summary>
        /// GetSoapAsyncState: Encapsulates the service in a state with an id and object
        /// </summary>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns>SoapAsyncState Object, containing the Client (Soap Client), ID (a string id), and Data (object passed)</returns>
        public static SoapAsyncState GetSoapAsyncState(object state=null)
        {
            var soapAsyncState = state as SoapAsyncState;
            if (state == null)
            {
                soapAsyncState = new SoapAsyncState();
            }
            else if (state is IOrganizationService)
            {
                soapAsyncState = new SoapAsyncState(state as IOrganizationService);
            }
            else if (soapAsyncState == null)
            {
                soapAsyncState = new SoapAsyncState { Data = state };
            }
            return soapAsyncState;
        }

        /// <summary>
        /// BeginCreate: Begin Asynchronous Create
        /// </summary>
        /// <param name="entity">The entity to create</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginCreate(Entity entity, AsyncCallback callback,
                                               object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            
            return soapAsyncState.Client.BeginCreate(entity, callback, soapAsyncState);
        }
        
        /// <summary>
        /// BeginDisassociate: Disassociate an entity from another entity.
        /// </summary>
        /// <param name="parentEntity">Parent entity reference</param>
        /// <param name="relationship">The relationship</param>
        /// <param name="relatedEntities">The related entities to detach</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginDisassociate(EntityReference parentEntity, Relationship relationship,
                                                     EntityReferenceCollection relatedEntities, AsyncCallback callback,
                                                     object state = null)
        {
            return BeginDisassociate(parentEntity.LogicalName, parentEntity.Id, relationship, relatedEntities, callback, state);
        }

        /// <summary>
        /// BeginDisassociate: Disassociate an entity from another entity.
        /// </summary>
        /// <param name="entityName">The name of the parent entity</param>
        /// <param name="entityId">The id of the parent entity</param>
        /// <param name="relationship">The relationship</param>
        /// <param name="relatedEntities">The related entities to detach</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginDisassociate(string entityName, Guid entityId, Relationship relationship,
                                                     EntityReferenceCollection relatedEntities, AsyncCallback callback,
                                                     object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginDisassociate(entityName, entityId, relationship, relatedEntities, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginUpdate: Update an entity
        /// </summary>
        /// <param name="entity">The entity to update (with changes)</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginUpdate(Entity entity, AsyncCallback callback,
                                               object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginUpdate(entity, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginExecute: Execute a request
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecute(OrganizationRequest request, AsyncCallback callback,
                                                object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginExecute(request, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginAssociate: Associate entities with the parent entity
        /// </summary>
        /// <param name="parentEntity">Parent entity reference</param>
        /// <param name="relationship">The relationship</param>
        /// <param name="relatedEntities">The related entities</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginAssociate(EntityReference parentEntity, Relationship relationship,
                                                  EntityReferenceCollection relatedEntities, AsyncCallback callback,
                                                  object state = null)
        {
            return BeginAssociate(parentEntity.LogicalName, parentEntity.Id, relationship, relatedEntities, callback, state);
        }

        /// <summary>
        /// BeginAssociate: Associate entities with the parent entity
        /// </summary>
        /// <param name="entityName">Parent entity name</param>
        /// <param name="entityId">Parent entity id</param>
        /// <param name="relationship">The relationship</param>
        /// <param name="relatedEntities">The related entities</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginAssociate(string entityName, Guid entityId, Relationship relationship,
                                                  EntityReferenceCollection relatedEntities, AsyncCallback callback,
                                                  object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginAssociate(entityName, entityId, relationship, relatedEntities, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginDelete: Delete an entity
        /// </summary>
        /// <param name="target">The target entity</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginDelete(EntityReference target, AsyncCallback callback,
                                               object state = null)
        {
            return BeginDelete(target.LogicalName, target.Id, callback, state);
        }

        /// <summary>
        /// BeginDelete: Delete an entity
        /// </summary>
        /// <param name="entityName">The name of the entity</param>
        /// <param name="id">The id of the entity</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginDelete(string entityName, Guid id, AsyncCallback callback,
                                               object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginDelete(entityName, id, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginRetrieve: Retrieve an entity and all columns
        /// </summary>
        /// <param name="entityName">The name of the entity</param>
        /// <param name="id">The id of the entity</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginRetrieve(string entityName, Guid id, AsyncCallback callback,
                                                 object state = null)
        {
            return BeginRetrieve(entityName, id, new ColumnSet { AllColumns = true }, callback, state);
        }

        /// <summary>
        /// BeginRetrieve: Retrieve an entity and all columns
        /// </summary>
        /// <param name="target">Target entity reference</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginRetrieve(EntityReference target, AsyncCallback callback,
                                                 object state = null)
        {
            return BeginRetrieve(target.LogicalName, target.Id, new ColumnSet { AllColumns = true }, callback, state);
        }

        /// <summary>
        /// BeginRetrieve: Retrieve an entity
        /// </summary>
        /// <param name="target">Target entity reference</param>
        /// <param name="columnSet">The columns to retrieve</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginRetrieve(EntityReference target, ColumnSet columnSet, AsyncCallback callback,
                                                 object state = null)
        {
            return BeginRetrieve(target.LogicalName, target.Id, columnSet, callback, state);
        }

        /// <summary>
        /// BeginRetrieve: Retrieve an entity
        /// </summary>
        /// <param name="entityName">The name of the entity</param>
        /// <param name="id">The id of the entity</param>
        /// <param name="columnSet">The columns to retrieve</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginRetrieve(string entityName, Guid id, ColumnSet columnSet, AsyncCallback callback,
                                                 object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginRetrieve(entityName, id, columnSet, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginRetrieveMultiple: Retrieve multiple entities
        /// </summary>
        /// <param name="query">The query</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginRetrieveMultiple(QueryExpression query, AsyncCallback callback, object state = null)
        {
            var soapAsyncState = GetSoapAsyncState(state);

            return soapAsyncState.Client.BeginRetrieveMultiple(query, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginExecuteSearchRequest: Uses Execute to run a Search Request for Appointment Proposals
        /// </summary>
        /// <param name="request">The appointment request</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteSearchRequest(AppointmentRequest request, AsyncCallback callback,
                                                                object state = null)
        {
            var or = new OrganizationRequest { RequestName = "Search" };
            or["AppointmentRequest"] = request;

            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginExecute(or, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginExecuteRetrieveMultiple: Uses Execute to RetrieveMultiple
        /// </summary>
        /// <param name="query">The query</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteRetrieveMultiple(QueryBase query, AsyncCallback callback,
                                                                object state = null)
        {
            var request = new OrganizationRequest { RequestName = "RetrieveMultiple" };
            request["Query"] = query;

            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginExecute(request, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginExecuteFetch: Uses Execute a FetchXml Query
        /// </summary>
        /// <param name="fetchXml">The fetchXml Query</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteFetch(Fetch fetchXml, AsyncCallback callback,
                                                     object state = null)
        {
            return BeginExecuteRetrieveMultiple(new FetchExpression { Query = fetchXml.Serialize() }, callback, state);
        }

        /// <summary>
        /// BeginExecuteFetch: Uses Execute a FetchXml Query
        /// </summary>
        /// <param name="fetchXml">The fetchXml Query</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteFetch(string fetchXml, AsyncCallback callback,
                                                     object state = null)
        {
            return BeginExecuteRetrieveMultiple(new FetchExpression { Query = fetchXml }, callback, state);
        }

        /// <summary>
        /// BeginExecuteRetrieveAllEntities: Uses Execute to retrieves all the metadata for all entities
        /// </summary>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteRetrieveAllEntities(AsyncCallback callback,
                                                            object state = null)
        {
            var request = new OrganizationRequest { RequestName = "RetrieveAllEntities" };
            request["EntityFilters"] = EntityFilters.Entity;
            request["RetrieveAsIfPublished"] = false;

            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginExecute(request, callback, soapAsyncState);
        }

        /// <summary>
        /// BeginExecuteRetrieveEntitiy: Uses Execute to retrieve an entity's metadata
        /// </summary>
        /// <param name="entityName">The entity name</param>
        /// <param name="callback">The callback method</param>
        /// <param name="state">The Soap client, AsyncState, or an object to be passed as data</param>
        /// <returns></returns>
        public static IAsyncResult BeginExecuteRetrieveEntity(string entityName, AsyncCallback callback,
                                                              object state = null)
        {
            var request = new OrganizationRequest { RequestName = "RetrieveEntity" };
            request["EntityFilters"] = EntityFilters.Attributes;
            request["LogicalName"] = entityName;
            request["MetadataId"] = Guid.Empty;
            request["RetrieveAsIfPublished"] = false;

            var soapAsyncState = GetSoapAsyncState(state);
            return soapAsyncState.Client.BeginExecute(request, callback, soapAsyncState);
        }

        /// <summary>
        /// Combines a URI with a relative URI
        /// </summary>
        /// <param name="baseUrl">Base (absolute) URI</param>
        /// <param name="path">Relative URI that is to be used</param>
        /// <returns>Combined URI</returns>
        private static Uri CombineUrl(Uri baseUrl, string path)
        {
            if (null == baseUrl)
            {
                throw new ArgumentNullException("baseUrl");
            }

            if (string.IsNullOrEmpty(path))
            {
                return baseUrl;
            }

            string url = baseUrl.AbsoluteUri;
            int end = url.Length - 1;

            // Check if there is a '/' at the beginning of path
            if (url[end] == '/' && path[0] == '/')
            {
                url = url.Substring(0, end);
            }
            else if (url[end] != '/' && path[0] != '/')
            {
                url += '/';
            }

            // Create the combined URL
            return new Uri(url + path);
        }
    }
}